Understanding Runtime and Compile-Time Polymorphism in Java
Course: Core Java Training
Topic: Understanding Runtime and Compile-Time Polymorphism
Course Link: Core Java Training Course
Demo Link: Register for a Course Demo
Website: JustAcademy
1. Introduction to Polymorphism in Java
Polymorphism is one of the four fundamental principles of object-oriented programming (OOP), along with inheritance, encapsulation, and abstraction. The word polymorphism means "many forms." In Java, polymorphism allows the same method name or common reference to be used in different ways depending on the method parameters or the actual object involved.
Polymorphism helps developers create flexible, reusable, maintainable, and extensible applications. Instead of writing separate logic for every variation of an operation, developers can use a common method name or superclass reference to represent different behaviors.
Java commonly demonstrates two forms of polymorphism:
- Compile-time polymorphism: Commonly achieved through method overloading.
- Runtime polymorphism: Commonly achieved through method overriding and dynamic method dispatch.
2. What Is Compile-Time Polymorphism?
Compile-time polymorphism is a form of polymorphism in which the compiler determines which overloaded method to call based on the method name, argument count, argument types, and applicable method-resolution rules.
It is also known as static polymorphism or static binding in introductory Java terminology. Method overloading is its most common example.
Key Characteristics
- It is commonly implemented using method overloading.
- Methods share the same name but have different parameter lists.
- The compiler selects the applicable overloaded method during compilation.
- It can make APIs easier to use by allowing one method name to support different input types or quantities.
- Inheritance is not required for basic method overloading.
3. Understanding Method Overloading
Method overloading occurs when a class contains multiple methods with the same name but different parameter lists. The methods may differ in the number of parameters, parameter types, or parameter order.
Example 1: Basic Method Overloading
class Calculator {
int add(int a, int b) {
return a + b;
}
int add(int a, int b, int c) {
return a + b + c;
}
double add(double a, double b) {
return a + b;
}
}
public class Main {
public static void main(String[] args) {
Calculator calculator = new Calculator();
System.out.println(calculator.add(10, 20));
System.out.println(calculator.add(10, 20, 30));
System.out.println(calculator.add(5.5, 4.5));
}
}
Output
30
60
10.0
Explanation
add(int, int) accepts two integer arguments.
add(int, int, int) accepts three integer arguments.
add(double, double) accepts two double arguments.
- The compiler selects the appropriate overload based on the supplied arguments and Java's method-resolution rules.
4. Rules of Method Overloading
To overload a method, its parameter list must be different from the other method's parameter list.
| Method Variation |
Valid Overloading? |
Explanation |
| Different number of parameters |
Yes |
For example, show(int) and show(int, int). |
| Different parameter types |
Yes |
For example, display(int) and display(String). |
| Different parameter order |
Yes |
For example, print(int, String) and print(String, int). |
| Different return type only |
No |
Changing only the return type does not create a valid overload. |
| Different parameter names only |
No |
Parameter names do not distinguish method signatures for overloading. |
Example 2: Overloading with Different Parameter Types
class Display {
void show(int number) {
System.out.println("Integer: " + number);
}
void show(String text) {
System.out.println("Text: " + text);
}
void show(double number) {
System.out.println("Decimal: " + number);
}
}
public class Main {
public static void main(String[] args) {
Display display = new Display();
display.show(100);
display.show("Java");
display.show(25.5);
}
}
Output
Integer: 100
Text: Java
Decimal: 25.5
5. Compile-Time Polymorphism with Constructors
Java also supports constructor overloading. A class can define multiple constructors with different parameter lists to initialize objects in different ways. The compiler determines which constructor is applicable when an object is created.
class Student {
String name;
int age;
Student() {
name = "Unknown";
age = 0;
}
Student(String name) {
this.name = name;
age = 18;
}
Student(String name, int age) {
this.name = name;
this.age = age;
}
void display() {
System.out.println(name + " - " + age);
}
}
public class Main {
public static void main(String[] args) {
Student s1 = new Student();
Student s2 = new Student("Amit");
Student s3 = new Student("Priya", 21);
s1.display();
s2.display();
s3.display();
}
}
Output
Unknown - 0
Amit - 18
Priya - 21
Each constructor has a distinct parameter list. Java chooses the applicable constructor according to the arguments passed during object creation.
6. Advantages of Compile-Time Polymorphism
- Readability: Related operations can share a meaningful method name.
- Convenience: Callers can pass different supported argument combinations.
- Code organization: Similar operations can be grouped under one method name.
- Type safety: The compiler checks method applicability and argument compatibility.
- Reusability: One class can provide several versions of a useful operation.
7. Limitations of Compile-Time Polymorphism
- Too many overloaded methods can make an API difficult to understand.
- Ambiguous overloads can cause compilation errors.
- Overloading alone does not let a subclass replace an inherited method's implementation for dynamic dispatch.
- Overloading decisions can be affected by compile-time argument types, widening conversions, boxing, and varargs rules.
8. What Is Runtime Polymorphism?
Runtime polymorphism is a form of polymorphism in which Java determines which overridden instance method implementation to execute at runtime based on the actual object's class.
It is also called dynamic polymorphism or dynamic method dispatch. It is commonly achieved through method overriding and inheritance, including implementations of interface methods.
Key Characteristics
- It commonly uses inheritance or interface implementation.
- A subclass provides an implementation of an inherited instance method.
- A superclass reference can refer to an instance of a subclass.
- For an overridden instance method, the implementation associated with the actual object is selected at runtime.
- It supports flexible designs where different object types can be handled through a common abstraction.
9. Understanding Method Overriding
Method overriding occurs when a subclass provides its own implementation of an inherited instance method using a compatible method signature and return type. The overriding method cannot reduce the access level of the overridden method.
The @Override annotation is recommended because it allows the compiler to detect mistakes when a method is intended to override an inherited method.
Example 3: Basic Method Overriding
class Animal {
void sound() {
System.out.println("Animal makes a sound.");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Dog barks.");
}
}
class Cat extends Animal {
@Override
void sound() {
System.out.println("Cat meows.");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
dog.sound();
cat.sound();
}
}
Output
Dog barks.
Cat meows.
Explanation
- Animal defines the common
sound() method.
- Dog overrides
sound() to represent a dog's sound.
- Cat overrides
sound() to represent a cat's sound.
- Calling
sound() on each object executes that class's implementation.
10. Runtime Polymorphism Using a Parent Reference
One of the most important features of runtime polymorphism is that a superclass reference can refer to a subclass object. The reference's declared type determines which members are available at compile time, while the actual object's class determines the overridden instance method implementation executed at runtime.
class Animal {
void sound() {
System.out.println("Animal sound.");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Dog barks.");
}
}
class Cat extends Animal {
@Override
void sound() {
System.out.println("Cat meows.");
}
}
public class Main {
public static void main(String[] args) {
Animal animal;
animal = new Dog();
animal.sound();
animal = new Cat();
animal.sound();
}
}
Output
Dog barks.
Cat meows.
The variable animal is declared as Animal, but it refers to a Dog object first and a Cat object second. Java invokes the overridden method associated with each actual object.
11. Dynamic Method Dispatch
Dynamic method dispatch is the mechanism Java uses to select an overridden instance method implementation at runtime. It enables runtime polymorphism.
How It Works
- A superclass defines an instance method.
- A subclass overrides that method.
- A superclass reference refers to an object of the subclass.
- The program invokes the method using the superclass reference.
- At runtime, Java selects the implementation belonging to the actual object's class.
Animal animal = new Dog();
animal.sound();
Here, animal has the declared type Animal, but the actual object is Dog. If Dog overrides sound(), Java executes Dog's version.
12. Runtime Polymorphism with Abstract Classes
An abstract class can define shared behavior and declare abstract methods that subclasses must implement. References of the abstract superclass type can then refer to different concrete subclass objects.
abstract class Shape {
abstract double calculateArea();
}
class Circle extends Shape {
double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
double calculateArea() {
return Math.PI * radius * radius;
}
}
class Rectangle extends Shape {
double length;
double width;
Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
@Override
double calculateArea() {
return length * width;
}
}
public class Main {
public static void main(String[] args) {
Shape shape1 = new Circle(3);
Shape shape2 = new Rectangle(4, 5);
System.out.printf("Circle area: %.2f%n",
shape1.calculateArea());
System.out.printf("Rectangle area: %.2f%n",
shape2.calculateArea());
}
}
Output
Circle area: 28.27
Rectangle area: 20.00
The same method call, calculateArea(), produces different results depending on whether the actual object is a Circle or Rectangle.
13. Runtime Polymorphism with Interfaces
Interfaces allow unrelated implementation classes to provide a common contract. Code can invoke interface methods without needing to know which concrete implementation it receives.
interface Payment {
void pay(double amount);
}
class CreditCardPayment implements Payment {
@Override
public void pay(double amount) {
System.out.println("Paid " + amount + " using credit card.");
}
}
class UpiPayment implements Payment {
@Override
public void pay(double amount) {
System.out.println("Paid " + amount + " using UPI.");
}
}
public class Main {
public static void main(String[] args) {
Payment payment;
payment = new CreditCardPayment();
payment.pay(1500);
payment = new UpiPayment();
payment.pay(750);
}
}
Output
Paid 1500.0 using credit card.
Paid 750.0 using UPI.
This example demonstrates how the same Payment interface can represent different payment implementations. A real payment system would also require appropriate validation, secure processing, and error handling.
14. Compile-Time vs Runtime Polymorphism
| Feature |
Compile-Time Polymorphism |
Runtime Polymorphism |
| Common mechanism |
Method overloading |
Method overriding |
| Method selection |
Compiler resolves the applicable overload |
Overridden instance method is selected at runtime |
| Parameter list |
Must differ for overloads |
Must satisfy overriding rules |
| Inheritance required |
No |
Usually uses inheritance or interfaces |
| Reference type |
Argument and reference types can affect overload resolution |
Actual object type selects the overridden implementation |
| Typical binding terminology |
Static binding |
Dynamic binding |
| Example |
add(int, int) and add(double, double) |
Dog and Cat override sound() |
| Main benefit |
Convenient method APIs for different inputs |
Flexible behavior through common parent types or interfaces |
15. Example Combining Overloading and Overriding
Java supports both forms in the same application. In the following example, the Animal class has overloaded methods, while Dog overrides one of its inherited methods.
class Animal {
void eat() {
System.out.println("Animal eats food.");
}
void eat(String food) {
System.out.println("Animal eats " + food + ".");
}
void sound() {
System.out.println("Animal makes a sound.");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Dog barks.");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.eat();
dog.eat("meat");
dog.sound();
}
}
Output
Animal eats food.
Animal eats meat.
Dog barks.
The eat() methods demonstrate overloading because their parameter lists differ. The sound() method demonstrates overriding because Dog provides a replacement implementation inherited from Animal.
16. Important Rules and Special Cases
- Return type alone is not enough for overloading: Two methods cannot differ only by return type.
- Overriding requires compatibility: The subclass method must follow Java's signature, return-type, and access rules.
- Static methods are hidden, not overridden: Static method calls are resolved using the compile-time type and method-hiding rules.
- Private methods are not overridden: A private method is not inherited as an overridable method by a subclass.
- Final methods cannot be overridden: A subclass cannot override a method declared
final.
- Overload resolution uses compile-time information: Java chooses an applicable overload using the declared types of expressions and its method-resolution rules.
- Overridden instance method dispatch uses the actual object: For ordinary overridable instance methods, the implementation is selected at runtime.
17. Common Mistakes to Avoid
- Assuming that changing only the return type creates an overloaded method.
- Confusing method overloading with method overriding.
- Forgetting to use
@Override when a method is intended to override a superclass method.
- Trying to override a final or private method.
- Assuming a superclass reference can directly access subclass-specific methods without an appropriate cast or another design approach.
- Assuming static methods use runtime polymorphism in the same way as overridden instance methods.
- Creating too many overloads with unclear or ambiguous argument combinations.
- Using inheritance where composition or an interface would be a better fit.
18. Best Practices
- Use method overloading when methods represent the same logical operation with different parameter lists.
- Use method overriding when a subclass needs to specialize inherited behavior.
- Use
@Override to make your intention explicit and catch mistakes.
- Design superclass and interface contracts carefully so that implementations behave consistently.
- Prefer programming to interfaces when different implementations should be interchangeable.
- Keep method names descriptive and overloaded versions easy to understand.
- Test both the common behavior and the behavior specific to each implementation.
- Use polymorphism to reduce repetitive conditional logic when object-specific behavior is naturally modeled by overridden methods.
19. Real-World Applications
| Application |
Compile-Time Example |
Runtime Example |
| Calculator |
Overloaded add() methods for different input types |
Different calculator implementations override a common operation |
| Payment System |
Overloaded methods accept different supported input formats |
Card, UPI, and other payment classes implement a common payment contract |
| Notification System |
Overloaded notification methods accept different parameter combinations |
Email and SMS notification classes provide their own delivery behavior |
| Graphics Application |
Overloaded drawing methods accept different argument lists |
Circle and Rectangle override a common drawing or area method |
| Employee Management |
Overloaded search methods accept different search criteria |
Employee subclasses provide specialized calculation behavior |
20. Practice Exercise 1: Compile-Time Polymorphism
Task: Create a class named Calculator with overloaded methods named multiply().
Requirements
- Create a method that multiplies two integers.
- Create another method that multiplies three integers.
- Create a method that multiplies two double values.
- Call all three methods from the main method.
- Display each result.
Expected Output
Product of two integers: 50
Product of three integers: 120
Product of decimal values: 10.0
21. Practice Exercise 2: Runtime Polymorphism
Task: Create a parent class named Employee and two subclasses named Developer and Manager.
Requirements
- Add a method named
work() to Employee.
- Override
work() in Developer and Manager.
- Create an Employee reference.
- Assign a Developer object to the reference and call
work().
- Assign a Manager object to the same reference and call
work().
Expected Output
Developer writes and tests code.
Manager plans work and coordinates the team.
22. Interview Questions and Answers
Q1. What is polymorphism in Java?
Answer: Polymorphism allows the same method name or common reference to represent different behaviors depending on parameters or the actual object.
Q2. What are the two commonly taught types of polymorphism in Java?
Answer: Compile-time polymorphism, commonly demonstrated by method overloading, and runtime polymorphism, commonly demonstrated by method overriding.
Q3. What is compile-time polymorphism?
Answer: It is polymorphism in which the compiler selects an applicable overloaded method based on compile-time information and Java's method-resolution rules.
Q4. What is runtime polymorphism?
Answer: It is polymorphism in which Java selects an overridden instance method implementation at runtime based on the actual object.
Q5. What is the difference between overloading and overriding?
Answer: Overloading uses the same method name with different parameter lists. Overriding occurs when a subclass supplies a compatible implementation of an inherited instance method.
Q6. Can we overload a method by changing only its return type?
Answer: No. The parameter list must differ; a return-type change alone does not create a valid overload.
Q7. Can static methods be overridden?
Answer: No. A static method declared in a subclass with the same signature hides the superclass's static method rather than overriding it.
Q8. What is dynamic method dispatch?
Answer: It is the runtime mechanism that selects the implementation of an overridden instance method based on the actual object.
Q9. Why is the @Override annotation useful?
Answer: It tells the compiler that a method is intended to override an inherited method, helping detect incorrect signatures and other mistakes.
Q10. Can runtime polymorphism work with interfaces?
Answer: Yes. A reference of an interface type can refer to different implementing objects, and overridden implementations can be selected at runtime.
Q11. Is inheritance required for method overloading?
Answer: No. A class can overload its own methods without extending another user-defined class.
Q12. What is the main benefit of runtime polymorphism?
Answer: It allows different object types to be handled through a common superclass or interface while executing their own implementations of shared methods.
23. Quick Revision Checklist
- Polymorphism means "many forms."
- Compile-time polymorphism is commonly achieved through method overloading.
- Overloaded methods must have different parameter lists.
- Changing only the return type does not create a valid overload.
- Runtime polymorphism is commonly achieved through method overriding.
- Overriding allows a subclass to specialize inherited instance behavior.
- A superclass reference can refer to a subclass object.
- The actual object determines which overridden instance method implementation runs.
- Static methods are hidden rather than overridden.
- The
@Override annotation helps detect mistakes.
- Interfaces can support runtime polymorphism.
- Both forms improve the design and usability of Java applications when used appropriately.
24. Conclusion
Compile-time and runtime polymorphism are essential concepts in Java object-oriented programming. Compile-time polymorphism, usually demonstrated by method overloading, lets developers use the same method name for different parameter lists. Runtime polymorphism, commonly demonstrated by method overriding, allows different objects to provide specialized implementations of a shared method.
Understanding the difference between overload resolution and dynamic method dispatch helps developers write cleaner, more reusable, and extensible Java applications. Practice both concepts with small programs before applying them to larger projects.
Learn Core Java with JustAcademy
Strengthen your Java fundamentals, object-oriented programming knowledge, and practical coding skills with the following resources.
Additional Learning Resource: Oracle Java Tutorial: Polymorphism